/**
 * This file is part of the Kompics P2P Framework.
 *
 * Copyright (C) 2009 Swedish Institute of Computer Science (SICS)
 * Copyright (C) 2009 Royal Institute of Technology (KTH)
 *
 * Kompics is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */
package se.sics.drsm.model;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import se.sics.drsm.components.DRSMIterativeRouter;
import se.sics.drsm.overlay_address.key.NumericRingKey;
import se.sics.drsm.overlay_address.key.RingKey.IntervalBounds;
import se.sics.drsm.util.CollectionUtil;

/**
 * The <code>FingerTable</code> class.
 *
 * @author Cosmin Arad <cosmin@sics.se>
 * @author Shadid Chowdhury <msrch@kth.se>
 */
public class DRSMFingerTable {

    //Shadid add a finger table dump method

    private int log2RingSize;

    private BigInteger ringSize;

    private DRSMAddress self;

    private List<DRSMAddress> localPeers;

    private NumericRingKey[] begin;

    private NumericRingKey[] end;

    private Map<Integer, List<DRSMAddress>> finger;

    private long[] lastUpdated;

    private DRSMIterativeRouter router;

    private int nextFingerToFix;

    public DRSMFingerTable(int log2RingSize, DRSMAddress localPeer,
                           DRSMIterativeRouter router) {
        this.router = router;

        this.log2RingSize = log2RingSize;
        ringSize = new BigInteger("2").pow(log2RingSize);
        this.self = localPeer;
        this.localPeers = new ArrayList<DRSMAddress>();
        this.localPeers.add(localPeer);

        begin = new NumericRingKey[log2RingSize];
        end = new NumericRingKey[log2RingSize];
        finger = new HashMap<Integer, List<DRSMAddress>>();
        lastUpdated = new long[log2RingSize];

        nextFingerToFix = -1;

        initFingerTable();
    }

    private void initFingerTable() {
        long now = System.currentTimeMillis();
        DRSMAddress peer = CollectionUtil.getFirstElem(localPeers);
        for (int i = 0; i < log2RingSize; i++) {
            begin[i] = new NumericRingKey(new BigInteger("2").pow(i).add(
                    peer.getKey().getId()).mod(ringSize));
            end[i] = new NumericRingKey(new BigInteger("2").pow(i + 1).add(
                    peer.getKey().getId()).mod(ringSize));
            finger.put(i, null); // TODO: Is this really needed.
            lastUpdated[i] = now;
        }
    }

    public int nextFingerToFix() {
        nextFingerToFix++;
        if (nextFingerToFix >= log2RingSize) {
            nextFingerToFix = 0;
        }
        return nextFingerToFix;
    }

    public void fingerFixed(int indexToFix, List<DRSMAddress> fingerPeers) {
        lastUpdated[indexToFix] = System.currentTimeMillis();
        finger.put(indexToFix, null);

        if (fingerPeers == null || fingerPeers.contains(localPeers)) {
            return;
        }
        if (CollectionUtil.getFirstElem(fingerPeers).getKey().belongsTo(begin[indexToFix], end[indexToFix],
                IntervalBounds.CLOSED_OPEN, ringSize)) {
            finger.put(indexToFix, fingerPeers);
        }
        router.fingerTableChanged();
    }

    public boolean learnedAboutFreshPeer(DRSMAddress newPeer) {
        return learnedAboutPeer(newPeer, true, true);
    }

    public boolean learnedAboutPeer(DRSMAddress newPeer) {
        return learnedAboutPeer(newPeer, true, false);
    }

    public boolean learnedAboutPeers(List<DRSMAddress> peerList, boolean update,
                                     boolean fresh) {
        boolean learned = false;
        for (DRSMAddress peer : peerList) {
            if (learnedAboutPeer(peer, update,
                    fresh))
                learned = true;

        }
        return learned;
    }

    public boolean learnedAboutPeer(DRSMAddress newPeer, boolean update,
                                    boolean fresh) {
        // we have learned about this new peer so we check whether it is a
        // better alternative for one of our fingers
        if (newPeer == null || localPeers.contains(newPeer)) {
            return false;
        }

        BigInteger distance = newPeer.getKey().ringMinus(CollectionUtil.getFirstElem(localPeers).getKey(),
                ringSize);
        int i = distance.bitLength() - 1;
        if (i == -1) {
            return false;
        }

        boolean changed = false;

        if (newPeer.getKey().belongsTo(begin[i], end[i],
                IntervalBounds.CLOSED_OPEN, ringSize)) {
            // it belongs to this interval
            // we update no matter what
            if (finger.get(i) == null) {

                finger.put(i, new ArrayList<DRSMAddress>());
                addToFingerTable(i, newPeer);
                changed = true;
            }
            if (fresh && newPeer.getKey().belongsTo(begin[i],
                    finger.get(i).get(0).getKey(),
                    IntervalBounds.CLOSED_OPEN, ringSize) && !finger
                    .get(i).get(0).getKey().equals(begin[i])) {
                // it is closer to the beginning of the interval than
                // the current finger
                addToFingerTable(i, newPeer);
                changed = true;
            }
        }

        if (changed && update) {
            router.fingerTableChanged();
        }
        return changed;
    }

    private void addToFingerTable(int index, DRSMAddress newPeer) {
        finger.get(index).add(newPeer);
        lastUpdated[index] = System.currentTimeMillis();
    }

    //
    public void fingerSuspected(DRSMAddress peer) {
        boolean changed = false;
        long now = System.currentTimeMillis();
        List<DRSMAddress> addressList;

        for (Integer i : finger.keySet()) {
            addressList = finger.get(i);
            if (addressList != null && addressList.contains(peer)) {
                addressList.remove(peer);
                lastUpdated[i] = now;
                changed = true;
            }
        }

        if (changed) {
            router.fingerTableChanged();
        }
    }

    public List<DRSMAddress> closestPreceedingPeers(NumericRingKey key) {
        List<DRSMAddress> addressList;
        DRSMAddress localPeer = CollectionUtil.getFirstElem(localPeers);
        for (int i = log2RingSize - 1; i >= 0; i--) {
            addressList = finger.get(i);
            if (addressList != null
                    && addressList
                    .get(0)
                    .getKey()
                    .belongsTo(localPeer.getKey(), key,
                            IntervalBounds.OPEN_OPEN, ringSize))
                return addressList;
        }
        return localPeers;
    }

    public DRSMFingerTableView getView() {

        Map<Integer, List<DRSMAddress>> fingerMap = new HashMap<Integer, List<DRSMAddress>>(finger);

        DRSMFingerTableView view = new DRSMFingerTableView(localPeers,
                begin.clone(), end.clone(), fingerMap, lastUpdated.clone());
        return view;
    }

    public NumericRingKey getFingerBegin(int fingerIndex) {
        return begin[fingerIndex];
    }

}